Skip to content

glcanon: rewrite the G-code preview on OpenGL 3.3 core - #4293

Open
alex-pres wants to merge 8 commits into
LinuxCNC:masterfrom
alex-pres:pr-gremlin-opengl-rewrite
Open

glcanon: rewrite the G-code preview on OpenGL 3.3 core#4293
alex-pres wants to merge 8 commits into
LinuxCNC:masterfrom
alex-pres:pr-gremlin-opengl-rewrite

Conversation

@alex-pres

Copy link
Copy Markdown

Replaces the fixed-function preview shared by AXIS, the GTK screens and QtVCP with a shader/VBO renderer: a baked trajectory buffer, an offscreen ID-buffer pass for picking, and a glyph atlas for overlay text. The GL matrix stack, display lists, immediate mode and GL_SELECT are gone.

@alex-pres

alex-pres commented Jul 30, 2026

Copy link
Copy Markdown
Author

Why rewrite?

  • OpenGL immediate mode has been deprecated for 18 years (since OpenGL 3.0 in 2008).
  • This unblocks many UI decisions — for example, it now works with XWayland.
  • There is a solid performance win, although it's only visible for large ngc files (engraving or 3D reliefs).

Performance

  • The new code does not make many OpenGL function calls per trajectory point. Instead, it builds a numpy array
    of trajectory points, transmits it directly from system memory to GPU memory, and submits (once) a small
    program to the GPU with instructions on how to convert it from 3D to 2D (the "shader" program). The same for backplot points.
  • Additionally, operations on trajectory points are optimized with numpy batch handling.
  • Selection of g-code line by clicking point must be also faster and GPU powered with new code.

Code Refactoring

Code is also reviewed and refactored for eaiser maintenance .

  • Previous code handled scene rotation from many places by directly calling OpenGL methods. Now there is a dedicated matrix that handle this.
  • Program siumlation reported points and Backlog points are separated to objects to easily see logic.
  • Also added new concept of Scene that contains Part(s) like Axes, Backplot, DRO, Program, and so on. Each part is self-contained and can be disabled, replaced or extended from code.
  • Previous version of code maintained arrays of moves, feeds and so on right in Python. These lists may take significant memory. For example look at comparsion below: | gcode.parse | old: +575 MB vs new:+28 MB | for 18M ngc file. New code does not need these lists and makes all necessary calculations while collecting points.

Hardware support

This rewrite targets the OpenGL 3.3 standard, released in 2010 and widely supported.

In case of hardware problems, or if drivers don't support the necessary features, the env var
LIBGL_ALWAYS_SOFTWARE=1 can be set to enable software OpenGL handling via llvmpipe.

Verification

  • During development, I tried to get pixel-to-pixel comparisons as much as possible and fix any differences.

Performance testing results

Benchmarked against stock master on fractal-1M.ngc (18.9 MB, 1,000,149 moves). Mac M4 ARM64, Docker + Xvfb + llvmpipe.

AXIS end-to-end (4 reps, real GUI, open_file_guts)

master rewrite
peak RSS 1674 MB 748 MB 2.24× lower
⤷ minus idle baseline (194 MB both) 1480 MB 554 MB 2.67× lower
steady redraw frame 0.32 s 0.12 s 2.7× faster
open_file_guts (open→drawn) 7.6 s (6.8–8.5) 7.1 s (7.1–7.4) a wash
load_preview alone 3.00 s 2.96 s a wash

Both screenshots confirm the file actually drew — this isn't one branch skipping work.

Phase harness (3 reps, EGL, isolates parse / extents / GPU build)

phase master rewrite
gcode.parse 1.66 s, +575 MB 2.76 s, +28 MB
calc_extents 0.86 s, +432 MB 0.00002 s, +0
GPU build 0.28 s (glNewList), +31 MB bake 0.027 s + upload 0.002 s
GL payload not queryable 24,003,600 B VBO (counted at glBufferData)
load total 2.82 s 2.80 s
peak RSS 1122 MB 276 MB
first click in preview +0.75 s, +276 MB (selection lists) none

What the numbers say

  • Memory is the headline. 2.2× lower peak in the real GUI, and the gap widens on first click: master compiles a second, per-line-named copy of the program into selection display lists (+276 MB); the rewrite's picker reads the buffers already uploaded.
  • Load time is unchanged, but for a non-obvious reason. The rewrite's raw parse is ~1.1 s slower — it transforms and fills arrays on the move path. That's fully paid back by calc_extents/unrotate_preview dropping from 0.86 s to zero, since the rewrite accumulates extents during the fill. If you want load time to actually drop, the parse hot path is where the remaining 1.1 s lives.
  • Frame time is 2.7× better and the GPU build is 10× cheaper (0.03 s vs 0.28 s).

Two caveats worth stating: llvmpipe puts display lists and VBOs in process RSS, so the RSS deltas are the only cross-branch-comparable "GPU memory" figure — a display list has no queryable size, so master gets no exact byte count. And I noticed that at the same view master's preview saturates to white where the rewrite renders mid-grey; that's a blend difference on densely overlapping geometry which I did not investigate.

@BsAtHome

Copy link
Copy Markdown
Contributor

Good work.

Functional touch screen support is rather important to keep working. Breaking a touch interface would probably make quite a few users pulling hairs and may even require baldness treatments when it persists.

Some cases that should function: running on RPi4, Rpi5 and running with remote X (also ssh tunneled)?

@alex-pres

alex-pres commented Jul 30, 2026

Copy link
Copy Markdown
Author

Good work.

Functional touch screen support is rather important to keep working. Breaking a touch interface would probably make quite a few users pulling hairs and may even require baldness treatments when it persists.

Some cases that should function: running on RPi4, Rpi5 and running with remote X (also ssh tunneled)?

I've just verified and touchscreen works exactly as in master branch, so no problems with it.

Unfortunately I have no RPi to test on it but I'd not expect any problems, this OpenGL API is not exotic. I'd say previous immediate mode API is less compatible.

Regarding remote X11 via SSH, my googling shows it may be a problem, but it is easy to solve by switching to software GL rendering with LIBGL_ALWAYS_SOFTWARE=1.

@c-morley

Copy link
Copy Markdown
Collaborator

Tried on my laptop, seems to work great - colors seem a bit darker. Very nice job!

@alex-pres
alex-pres marked this pull request as draft July 31, 2026 09:51
@alex-pres

alex-pres commented Jul 31, 2026

Copy link
Copy Markdown
Author

Seems some changes are necessary to make it RPi4B compatible. it seems to be easy. And even more important, it seems we can do much further with this and loading 1M+ points program can be handled well even on weak hardware. I'll work it out and submit here.

@snowgoer540

Copy link
Copy Markdown
Contributor

Gave this a go with the QtPlasmaC screen. All looks well except two things:

  1. I notice that the line width is thinner for the code preview, the position tracker, etc.
  2. QtPlasmaC uses glTranslatef to calculate a "table view". I made the following changes that seemed to work (being 100% honest, this is outside of my paygrade, so I used Claude to get here). I have no idea if this is the right way to do it or not. Completely welcome your experties!

Deleted:
from OpenGL.GL import glTranslatef

Added
from glnav import multiply, translation_matrix

Deleted:
glTranslatef(-xTableCenter, -yTableCenter, 0)

Added
widget.modelview = multiply(widget.modelview, translation_matrix(-xTableCenter, -yTableCenter, 0))

Otherwise it seems to work as expected!

@alex-pres

alex-pres commented Aug 1, 2026

Copy link
Copy Markdown
Author

Hi!

  1. I notice that the line width is thinner for the code preview, the position tracker, etc.

That is already fixed and I will submit this with updated PR. There was alpha blending set different. Now it pixel-to-pixel matches previous results.

  1. QtPlasmaC uses glTranslatef to calculate a "table view". I made the following changes that seemed to work (being 100% honest, this is outside of my paygrade, so I used Claude to get here). I have no idea if this is the right way to do it or not. Completely welcome your experties!

Thank you, I will include this for sure.

Replaces the fixed-function preview shared by AXIS, the GTK screens and
QtVCP with a shader/VBO renderer: a baked trajectory buffer, an offscreen
ID-buffer pass for picking, and a glyph atlas for overlay text. The GL
matrix stack, display lists, immediate mode and GL_SELECT are gone.
Per-move C->Python callbacks are about two thirds of a 1M-move parse. A
canon may now set use_move_batches and supply move_batch, receiving moves
and numeric non-move events as fixed-width float64 rows in bulk through a
read-only memoryview that reproduces the legacy values bit for bit. Canons
that do not opt in keep the call-per-move protocol unchanged.
glnav no longer imports OpenGL at module level, so the camera and its
matrix helpers import and test on a host with no GL stack. The
fixed-function first-expose lighting moves to an rs274.OpenGLTk.Opengl
override that owns the compatibility context, the dead display-list font
path goes, and scale() no longer raises NameError on an undefined name.
program_parts copied every plane array so the foam Z offset could be added
without mutating the canon array - even when the offset was 0.0, which is
every non-foam config. The offset is a rigid Z translation, so it folds into
the recorded pass MVP instead, at one site covering the colour, id and
override passes. Naming geometry.index also built that lazy property on the
load path, for something only the highlight reads; it is resolved on first
draw now.

Measured at 1M vertices: 16.0 MB copied -> 0, 32.0 MB -> 0 in foam. At the
10M size the buffer layout is sized against, 160 MB of copy and 227 MB of
index transient leave the window in which the driver is asked for 240 MB.
The VBO itself is unchanged at 24 bytes a move, 40 in foam.
Targeting GL 3.3 core alone ruled out the Raspberry Pi 4 the project ships
an official image for: Mesa v3d exposes no desktop core profile at all, and
its real API is OpenGL ES 3.1. The renderer now targets the intersection of
the two - one implementation, not a second. The GLSL version directive is
injected at compile time, the pick pass reads a colour attachment because ES
forbids reading depth, glMultiDrawArrays becomes a per-span loop, and narrow
buffers quad-expand when the driver refuses the line width asked for.
Context creation tries 3.3 core first and falls back to GLES 3.1. Desktop
rendering is unchanged: 0 differing pixels across all 17 toggle fixtures.
Compared against a stock-master build rather than against the previous
state of this rewrite, the program was compositing alpha whether or not the
toggle asked for it - feed pixels landing at (85,85,85) instead of white on
76,763 px of fractal-1M. Blending is gated on the toggle again.

With rapids solid the per-vertex dash distance became unreachable, so it and
its uniforms go: VERTEX_STRIDE 24 -> 20 bytes, 40 -> 32 in foam. The fill
then stopped asking numpy for reductions in the shape it is worst at - the
box of 16384 points costs 258 us whole-array against 22 us column by column
- for about 4x, producing identical output.
GlNavBase.modelview is now the only camera state and both consumers reload
the GL modelview from it every frame, so any glTranslatef() a screen issued
outside the camera was discarded on the next frame - the bundled plasmac
table view had silently stopped centring. Add translate_modelview(), which
post-multiplies exactly as glTranslatef() did, and convert the two bundled
call sites. Retires the direct GL access the rewrite left behind: hershey
and axis.py no longer import OpenGL, and emcmodule drops the epoxy link.
@alex-pres
alex-pres force-pushed the pr-gremlin-opengl-rewrite branch from cfb284d to f78ed6f Compare August 3, 2026 12:31
@alex-pres
alex-pres marked this pull request as ready for review August 3, 2026 12:33
@alex-pres

alex-pres commented Aug 3, 2026

Copy link
Copy Markdown
Author

I've updated the code, I think that is now works as it should. I've added the following:

  • Added [optional] BatchProtocol between C++ Gcode parser and Python Canon. That way, instead of multiple Python method calls and type conversions, C++ code creates array of floats in memory and sometimes pass pointer of it to Python code. That speeds up big file parsing a lot!
  • Added compatibility with OpenGL version used by RPI4 (OpenGL ES 3.2) . In fact, almost nothing changed besides OpenGL shader program headers (it must contain version as first comment).
  • FIxed alpha opacity bug that made picture different compared to previous code. Currently it is almost 100% pixel-to-pixel equal.
  • Made another optimization [ for simple usual case of NGC engraving or 3d relieph file ] if array of moves does not have toolchages inside or coordinate system rotation, it is handled more efficiently.

Final result: 5M move file is loading much faster.

P.S. During development I found surprising results - regardless of all my optimizations, big ngc file load was still slow and memory eager. I found the reason - AXIS code is not well optimised, and while parsing it eats ~500 bytes per every source code line, it is all with dynamic memory allocation. So if want to directly compare the gremlin/glcanon speed, use qtaxis it seems to be better optimized around this.

If you, like me, do not have big .ngc files to try it out, I put some to Dropbox: https://www.dropbox.com/scl/fo/sp3njw8t2hlujo65ql4ma/AMGK-jRiwfUF3G1QJ-4aY8U?rlkey=ewxjww93bbobqd0sb5nxbk289&st=3lribdca&dl=0

@alex-pres

alex-pres commented Aug 3, 2026

Copy link
Copy Markdown
Author

Some Benchmark results

Tested in AXIS with small patch that disables source code parsing [ by AXIS code ].
Used .ngc files from https://www.dropbox.com/scl/fo/sp3njw8t2hlujo65ql4ma/AMGK-jRiwfUF3G1QJ-4aY8U?rlkey=ewxjww93bbobqd0sb5nxbk289&st=3lribdca&dl=0

case moves open_file_guts (s) preview build (s) peak RSS (MB) 1st click (s) 2nd click (s) lines selected
master-1M 1,000,149 3.95 3.44 1533 1.239 0.375 none (8 attempts)
rewrite-1M 1,000,149 3.20 2.91 454 0.146 0.133 53518, 2083
master-5M 5,000,149 25.43 22.51 6643 12.396 1.921 none (6 attempts)
rewrite-5M 5,000,149 17.47 14.79 1079 0.370 0.318 8346, 8377

Load: 1.23x faster at 1M, 1.46x at 5M. Click-to-line: 8.5x faster at 1M, 33x at 5M — and only the rewrite actually selects anything. Master's first click is dominated by building the selection display lists (0.849 s at 1M, 10.35 s at 5M), then it discards the result: its 100-entry GL_SELECT hit buffer overflows on this geometry, so all 8 (1M) and 6 (5M) attempts highlighted nothing. Memory, 3.4x and 6.2x lower.

A canon that answers unknown attributes with a stub - the catch-all
__getattr__ idiom, as tests/interp_initcode's canon uses - handed back a
callable for both use_move_batches and move_batch and was opted into the
batch protocol without asking, silently dropping its moves into the stub.
@alex-pres
alex-pres force-pushed the pr-gremlin-opengl-rewrite branch from be39bbe to 00bedfd Compare August 3, 2026 15:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants